﻿/**
 * --------------------------------------------------------------------------
 * Bootstrap (v5.2.2): util/index.js
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
 * --------------------------------------------------------------------------
 */

const MAX_UID = 1000000
const MILLISECONDS_MULTIPLIER = 1000
const TRANSITION_END = 'transitionend'

// Shout-out Angus Croll (https://goo.gl/pxwQGp)
const toType = object => {
    if (object === null || object === undefined) {
        return `${object}`
    }

    return Object.prototype.toString.call(object).match(/\s([a-z]+)/i)[1].toLowerCase()
}

/**
 * Public Util API
 */

const getUID = prefix => {
    do {
        prefix += Math.floor(Math.random() * MAX_UID)
    } while (document.getElementById(prefix))

    return prefix
}

const getSelector = element => {
    let selector = element.getAttribute('data-bs-target')

    if (!selector || selector === '#') {
        let hrefAttribute = element.getAttribute('href')

        // The only valid content that could double as a selector are IDs or classes,
        // so everything starting with `#` or `.`. If a "real" URL is used as the selector,
        // `document.querySelector` will rightfully complain it is invalid.
        // See https://github.com/twbs/bootstrap/issues/32273
        if (!hrefAttribute || (!hrefAttribute.includes('#') && !hrefAttribute.startsWith('.'))) {
            return null
        }

        // Just in case some CMS puts out a full URL with the anchor appended
        if (hrefAttribute.includes('#') && !hrefAttribute.startsWith('#')) {
            hrefAttribute = `#${hrefAttribute.split('#')[1]}`
        }

        selector = hrefAttribute && hrefAttribute !== '#' ? hrefAttribute.trim() : null
    }

    return selector
}

const getSelectorFromElement = element => {
    const selector = getSelector(element)

    if (selector) {
        return document.querySelector(selector) ? selector : null
    }

    return null
}

const getElementFromSelector = element => {
    const selector = getSelector(element)

    return selector ? document.querySelector(selector) : null
}

const getTransitionDurationFromElement = element => {
    if (!element) {
        return 0
    }

    // Get transition-duration of the element
    let { transitionDuration, transitionDelay } = window.getComputedStyle(element)

    const floatTransitionDuration = Number.parseFloat(transitionDuration)
    const floatTransitionDelay = Number.parseFloat(transitionDelay)

    // Return 0 if element or transition duration is not found
    if (!floatTransitionDuration && !floatTransitionDelay) {
        return 0
    }

    // If multiple durations are defined, take the first
    transitionDuration = transitionDuration.split(',')[0]
    transitionDelay = transitionDelay.split(',')[0]

    return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER
}

const triggerTransitionEnd = element => {
    element.dispatchEvent(new Event(TRANSITION_END))
}

const isElement = object => {
    if (!object || typeof object !== 'object') {
        return false
    }

    if (typeof object.jquery !== 'undefined') {
        object = object[0]
    }

    return typeof object.nodeType !== 'undefined'
}

const getElement = object => {
    // it's a jQuery object or a node element
    if (isElement(object)) {
        return object.jquery ? object[0] : object
    }

    if (typeof object === 'string' && object.length > 0) {
        return document.querySelector(object)
    }

    return null
}

const getElementById = object => {
    if (typeof object === 'string' && object.length > 0 && object.substring(0, 1) !== '.' && object.substring(0, 1) !== '#') {
        object = `#${object}`
    }

    return getElement(object);
}

const isVisible = element => {
    if (!isElement(element) || element.getClientRects().length === 0) {
        return false
    }

    const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible'
    // Handle `details` element as its content may falsie appear visible when it is closed
    const closedDetails = element.closest('details:not([open])')

    if (!closedDetails) {
        return elementIsVisible
    }

    if (closedDetails !== element) {
        const summary = element.closest('summary')
        if (summary && summary.parentNode !== closedDetails) {
            return false
        }

        if (summary === null) {
            return false
        }
    }

    return elementIsVisible
}

const isDisabled = element => {
    if (!element || element.nodeType !== Node.ELEMENT_NODE) {
        return true
    }

    if (element.classList.contains('disabled')) {
        return true
    }

    if (typeof element.disabled !== 'undefined') {
        return element.disabled
    }

    return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'
}

const findShadowRoot = element => {
    if (!document.documentElement.attachShadow) {
        return null
    }

    // Can find the shadow root otherwise it'll return the document
    if (typeof element.getRootNode === 'function') {
        const root = element.getRootNode()
        return root instanceof ShadowRoot ? root : null
    }

    if (element instanceof ShadowRoot) {
        return element
    }

    // when we don't find a shadow root
    if (!element.parentNode) {
        return null
    }

    return findShadowRoot(element.parentNode)
}

const noop = () => { }

/**
 * Trick to restart an element's animation
 *
 * @param {HTMLElement} element
 * @return void
 *
 * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
 */
const reflow = element => {
    element.offsetHeight // eslint-disable-line no-unused-expressions
}

const getjQuery = () => {
    if (window.jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
        return window.jQuery
    }

    return null
}

const DOMContentLoadedCallbacks = []

const onDOMContentLoaded = callback => {
    if (document.readyState === 'loading') {
        // add listener on the first call when the document is in loading state
        if (!DOMContentLoadedCallbacks.length) {
            document.addEventListener('DOMContentLoaded', () => {
                for (const callback of DOMContentLoadedCallbacks) {
                    callback()
                }
            })
        }

        DOMContentLoadedCallbacks.push(callback)
    } else {
        callback()
    }
}

const isRTL = () => document.documentElement.dir === 'rtl'

const defineJQueryPlugin = plugin => {
    onDOMContentLoaded(() => {
        const $ = getjQuery()
        /* istanbul ignore if */
        if ($) {
            const name = plugin.NAME
            const JQUERY_NO_CONFLICT = $.fn[name]
            $.fn[name] = plugin.jQueryInterface
            $.fn[name].Constructor = plugin
            $.fn[name].noConflict = () => {
                $.fn[name] = JQUERY_NO_CONFLICT
                return plugin.jQueryInterface
            }
        }
    })
}

const execute = callback => {
    if (typeof callback === 'function') {
        callback()
    }
}

const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
    if (!waitForTransition) {
        execute(callback)
        return
    }

    const durationPadding = 5
    const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding

    let called = false

    const handler = ({ target }) => {
        if (target !== transitionElement) {
            return
        }

        called = true
        transitionElement.removeEventListener(TRANSITION_END, handler)
        execute(callback)
    }

    transitionElement.addEventListener(TRANSITION_END, handler)
    setTimeout(() => {
        if (!called) {
            triggerTransitionEnd(transitionElement)
        }
    }, emulatedDuration)
}

/**
 * Return the previous/next element of a list.
 *
 * @param {array} list    The list of elements
 * @param activeElement   The active element
 * @param shouldGetNext   Choose to get next or previous element
 * @param isCycleAllowed
 * @return {Element|elem} The proper element
 */
const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {
    const listLength = list.length
    let index = list.indexOf(activeElement)

    // if the element does not exist in the list return an element
    // depending on the direction and if cycle is allowed
    if (index === -1) {
        return !shouldGetNext && isCycleAllowed ? list[listLength - 1] : list[0]
    }

    index += shouldGetNext ? 1 : -1

    if (isCycleAllowed) {
        index = (index + listLength) % listLength
    }

    return list[Math.max(0, Math.min(index, listLength - 1))]
}

export {
    defineJQueryPlugin,
    execute,
    executeAfterTransition,
    findShadowRoot,
    getElement,
    getElementById,
    getElementFromSelector,
    getjQuery,
    getNextActiveElement,
    getSelectorFromElement,
    getTransitionDurationFromElement,
    getUID,
    isDisabled,
    isElement,
    isRTL,
    isVisible,
    noop,
    onDOMContentLoaded,
    reflow,
    triggerTransitionEnd,
    toType
}
